﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Clarity.Phone.Interactivity.Input.Dollar
{
	public class Utils
	{
		#region Constants

		private static readonly Random Rand = new Random();

		#endregion

		#region Lengths and Rects

		//public static RectangleR FindBox(ArrayList points)
		public static RectangleR FindBox(List<PointR> points)
		{
			var minX = double.MaxValue;
			var maxX = double.MinValue;
			var minY = double.MaxValue;
			var maxY = double.MinValue;
		
			foreach (var p in points)
			{
				if (p.X < minX)
					minX = p.X;
				if (p.X > maxX)
					maxX = p.X;
			
				if (p.Y < minY)
					minY = p.Y;
				if (p.Y > maxY)
					maxY = p.Y;
			}
		
			return new RectangleR(minX, minY, maxX - minX, maxY - minY);
		}

		public static double Distance(PointR p1, PointR p2)
		{
			var dx = p2.X - p1.X;
			var dy = p2.Y - p1.Y;
			return Math.Sqrt(dx * dx + dy * dy);
		}

		// compute the centroid of the points given
		//public static PointR Centroid(ArrayList points)
		public static PointR Centroid(List<PointR> points)
		{
			var xsum = 0.0;
			var ysum = 0.0;

			foreach (var p in points)
			{
				xsum += p.X;
				ysum += p.Y;
			}
			return new PointR(xsum / points.Count, ysum / points.Count);
		}

		//public static double PathLength(ArrayList points)
		public static double PathLength(List<PointR> points)
		{
			double length = 0;
			for (var i = 1; i < points.Count; i++)
			{
				length += Distance(points[i - 1], points[i]);
			}
			return length;
		}

		#endregion

		#region Angles and Rotations

		// determines the angle, in degrees, between two points. the angle is defined 
		// by the circle centered on the start point with a radius to the end point, 
		// where 0 degrees is straight right from start (+x-axis) and 90 degrees is
		// straight down (+y-axis).
		public static double AngleInDegrees(PointR start, PointR end, bool positiveOnly)
		{
			double radians = AngleInRadians(start, end, positiveOnly);
			return Rad2Deg(radians);
		}

		// determines the angle, in radians, between two points. the angle is defined 
		// by the circle centered on the start point with a radius to the end point, 
		// where 0 radians is straight right from start (+x-axis) and PI/2 radians is
		// straight down (+y-axis).
		public static double AngleInRadians(PointR start, PointR end, bool positiveOnly)
		{
			double radians = 0.0;
			if (start.X != end.X)
			{
				radians = Math.Atan2(end.Y - start.Y, end.X - start.X);
			}
			else // pure vertical movement
			{
				if (end.Y < start.Y)
					radians = -Math.PI / 2.0; // -90 degrees is straight up
				else if (end.Y > start.Y)
					radians = Math.PI / 2.0; // 90 degrees is straight down
			}
			if (positiveOnly && radians < 0.0)
			{
				radians += Math.PI * 2.0;
			}
			return radians;
		}

		public static double Rad2Deg(double rad)
		{
			return (rad * 180d / Math.PI);
		}

		public static double Deg2Rad(double deg)
		{
			return (deg * Math.PI / 180d);
		}

		// rotate the points by the given degrees about their centroid
		//public static ArrayList RotateByDegrees(ArrayList points, double degrees)
		public static List<PointR> RotateByDegrees(List<PointR> points, double degrees)
		{
			var radians = Deg2Rad(degrees);
			return RotateByRadians(points, radians);
		}

		// rotate the points by the given radians about their centroid
		//public static ArrayList RotateByRadians(ArrayList points, double radians)
		public static List<PointR> RotateByRadians(List<PointR> points, double radians)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var c = Centroid(points);

			var cos = Math.Cos(radians);
			var sin = Math.Sin(radians);

			var cx = c.X;
			var cy = c.Y;

			foreach (var t in points)
			{
				var p = t;

				var dx = p.X - cx;
				var dy = p.Y - cy;

				var q = PointR.Empty;
				q.X = dx * cos - dy * sin + cx;
				q.Y = dx * sin + dy * cos + cy;
				
				newPoints.Add(q);
			}
			return newPoints;
		}

		// Rotate a point 'p' around a point 'c' by the given radians.
		// Rotation (around the origin) amounts to a 2x2 matrix of the form:
		//
		//		[ cos A		-sin A	] [ p.x ]
		//		[ sin A		cos A	] [ p.y ]
		//
		// Note that the C# Math coordinate system has +x-axis stright right and
		// +y-axis straight down. Rotation is clockwise such that from +x-axis to
		// +y-axis is +90 degrees, from +x-axis to -x-axis is +180 degrees, and 
		// from +x-axis to -y-axis is -90 degrees.
		public static PointR RotatePoint(PointR p, PointR c, double radians)
		{
			var q = PointR.Empty;
			q.X = (p.X - c.X) * Math.Cos(radians) - (p.Y - c.Y) * Math.Sin(radians) + c.X;
			q.Y = (p.X - c.X) * Math.Sin(radians) + (p.Y - c.Y) * Math.Cos(radians) + c.Y;
			return q;
		}

		#endregion

		#region Translations

		// translates the points so that the upper-left corner of their bounding box lies at 'toPt'
		//public static ArrayList TranslateBBoxTo(ArrayList points, PointR toPt)
		public static List<PointR> TranslateBBoxTo(List<PointR> points, PointR toPt)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var r = FindBox(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X += (toPt.X - r.X);
				p.Y += (toPt.Y - r.Y);
				newPoints.Add(p);
			}
			return newPoints;
		}

		// translates the points so that their centroid lies at 'toPt'
		//public static ArrayList TranslateCentroidTo(ArrayList points, PointR toPt)
		public static List<PointR> TranslateCentroidTo(List<PointR> points, PointR toPt)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var centroid = Centroid(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X += (toPt.X - centroid.X);
				p.Y += (toPt.Y - centroid.Y);
				newPoints.Add(p);
			}
			return newPoints;
		}

		// translates the points by the given delta amounts
		//public static ArrayList TranslateBy(ArrayList points, SizeR sz)
		public static List<PointR> TranslateBy(List<PointR> points, SizeR sz)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X += sz.Width;
				p.Y += sz.Height;
				newPoints.Add(p);
			}
			return newPoints;
		}

		#endregion

		#region Scaling

		/// <summary>
		/// Scales the bounding box defined by a set of points after first rotating that set
		/// such that the angle from centroid-to-first-point is zero degrees. After scaling,
		/// the points are rotated back by the same angle.
		/// </summary>
		/// <param name="points"></param>
		/// <param name="sz"></param>
		/// <returns></returns>
		//public static ArrayList ScaleOrientedTo(ArrayList points, SizeR sz)
		public static List<PointR> ScaleOrientedTo(List<PointR> points, SizeR sz)
		{
			var radians = AngleInRadians(Centroid(points), points[0], false); // indicative angle
			//ArrayList newpoints = RotateByRadians(points, -radians); // rotate so that centroid-to-point[0] is 0 deg.
			var newpoints = RotateByRadians(points, -radians); // rotate so that centroid-to-point[0] is 0 deg.
			newpoints = ScaleTo(newpoints, sz);
			newpoints = RotateByRadians(newpoints, +radians); // restore orientation
			return newpoints;
		}

		// scales the points so that they form the size given. does not restore the 
		// origin of the box.
		//public static ArrayList ScaleTo(ArrayList points, SizeR sz)
		public static List<PointR> ScaleTo(List<PointR> points, SizeR sz)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var r = FindBox(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				if (r.Width != 0.0)
					p.X *= (sz.Width / r.Width);
				if (r.Height != 0.0)
					p.Y *= (sz.Height / r.Height);
				newPoints.Add(p);
			}
			return newPoints;
		}

		// scales by the percentages contained in the 'sz' parameter. values of 1.0 would result in the
		// identity scale (that is, no change).
		//public static ArrayList ScaleBy(ArrayList points, SizeR sz)
		public static List<PointR> ScaleBy(List<PointR> points, SizeR sz)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			FindBox(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X *= sz.Width;
				p.Y *= sz.Height;
				newPoints.Add(p);
			}
			return newPoints;
		}

		// scales the points so that the length of their longer side
		// matches the length of the longer side of the given box.
		// thus, both dimensions are warped proportionally, rather than
		// independently, like in the function ScaleTo.
		//public static ArrayList ScaleToMax(ArrayList points, RectangleR box)
		public static List<PointR> ScaleToMax(List<PointR> points, RectangleR box)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var r = FindBox(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X *= (box.MaxSide / r.MaxSide);
				p.Y *= (box.MaxSide / r.MaxSide);
				newPoints.Add(p);
			}
			return newPoints;
		}

		// scales the points so that the length of their shorter side
		// matches the length of the shorter side of the given box.
		// thus, both dimensions are warped proportionally, rather than
		// independently, like in the function ScaleTo.
		//public static ArrayList ScaleToMin(ArrayList points, RectangleR box)
		public static List<PointR> ScaleToMin(List<PointR> points, RectangleR box)
		{
			//ArrayList newPoints = new ArrayList(points.Count);
			var newPoints = new List<PointR>(points.Count);
			var r = FindBox(points);
			for (var i = 0; i < points.Count; i++)
			{
				var p = points[i];
				p.X *= (box.MinSide / r.MinSide);
				p.Y *= (box.MinSide / r.MinSide);
				newPoints.Add(p);
			}
			return newPoints;
		}

		#endregion

		#region Path Sampling and Distance

		//public static ArrayList Resample(ArrayList points, int n)
		public static List<PointR> Resample(List<PointR> points, int n)
		{
			var I = PathLength(points) / (n - 1); // interval length
			var D = 0.0;
			//ArrayList srcPts = new ArrayList(points);
			var srcPts = new List<PointR>(points);
			//ArrayList dstPts = new ArrayList(n);
			var dstPts = new List<PointR>(n) {srcPts[0]};
			for (var i = 1; i < srcPts.Count; i++)
			{
				var pt1 = srcPts[i - 1];
				var pt2 = srcPts[i];

				var d = Distance(pt1, pt2);
				if ((D + d) >= I)
				{
					var qx = pt1.X + ((I - D) / d) * (pt2.X - pt1.X);
					var qy = pt1.Y + ((I - D) / d) * (pt2.Y - pt1.Y);
					var q = new PointR(qx, qy);
					dstPts.Add(q); // append new point 'q'
					srcPts.Insert(i, q); // insert 'q' at position i in points s.t. 'q' will be the next i
					D = 0.0;
				}
				else
				{
					D += d;
				}
			}
			// somtimes we fall a rounding-error short of adding the last point, so add it if so
			if (dstPts.Count == n - 1)
			{
				dstPts.Add(srcPts[srcPts.Count - 1]);
			}

			return dstPts;
		}

		// computes the 'distance' between two point paths by summing their corresponding point distances.
		// assumes that each path has been resampled to the same number of points at the same distance apart.
		//public static double PathDistance(ArrayList path1, ArrayList path2)
		public static double PathDistance(List<PointR> path1, List<PointR> path2)
		{
			var distance = path1.Select((t, i) => Distance(t, path2[i])).Sum();
			return distance / path1.Count;
		}

		#endregion

		#region Random Numbers

		/// <summary>
		/// Gets a random number between low and high, inclusive.
		/// </summary>
		/// <param name="low"></param>
		/// <param name="high"></param>
		/// <returns></returns>
		public static int Random(int low, int high)
		{
			return Rand.Next(low, high + 1);
		}

		/// <summary>
		/// Gets multiple random numbers between low and high, inclusive. The
		/// numbers are guaranteed to be distinct.
		/// </summary>
		/// <param name="low"></param>
		/// <param name="high"></param>
		/// <param name="num"></param>
		/// <returns></returns>
		public static int[] Random(int low, int high, int num)
		{
			var array = new int[num];
			for (var i = 0; i < num; i++)
			{
				array[i] = Rand.Next(low, high + 1);
				for (var j = 0; j < i; j++)
				{
					if (array[i] != array[j])
						continue;

					i--; // redo i
					break;
				}
			}
			return array;
		}

		#endregion
	}
}
